﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Spanner
{
    public partial class UI
    {
        /// <summary>
        /// A JavaScript action constructed from a formatted string.
        /// The nth # in the fmt string corresponds to the nth item
        /// in the args.
        /// </summary>
        /// <param name="fmt"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public static Act JSAct(string fmt, params object[] args)
        {
            return new JSAct(fmt, args.Cast<ITerm>().ToArray());
        }

        /// <summary>
        /// The no-operation JavaScript action.
        /// </summary>
        /// <returns></returns>
        public static Act Noop()
        {
            return new JSAct("{} /* No op. */");
        }

        /// <summary>
        /// Set the contents of the observable x to e.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="x"></param>
        /// <param name="e"></param>
        /// <returns></returns>
        public static Act Set<T>(ObsVar<T> x, Expr<T> e)
        {
            return new JSAct("#(#)", Ref(x), e);
        }

        /// <summary>
        /// Set the observable x to the observable reference e.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="x"></param>
        /// <param name="e"></param>
        /// <returns></returns>
        public static Act Set<T>(ObsVar<T> x, Expr<ObsVarRef<T>> e)
        {
            return new JSAct("# = #", Ref(x), e);
        }

        public static Act Set<T>(Var<T> x, Expr<T> e)
        {
            return new JSAct("# = #", x, e);
        }

        public static Act Set<T>(Var<ObsVarRef<T>> x, Expr<T> e)
        {
            return new JSAct("#(#)", x, e);
        }

        public static Act Set<T>(Param<T> x, Expr<T> e)
        {
            return new JSAct("# = #", x, e);
        }

        public static Act Set<T>(Param<ObsVarRef<T>> x, Expr<T> e)
        {
            return new JSAct("#(#)", x, e);
        }

        public static Act Set<T>(IndexExpr<T> x, Expr<T> e)
        {
            return new JSAct("# = #", x, e);
        }

        public static Act Set<T>(IndexExpr<ObsVarRef<T>> x, Expr<T> e)
        {
            return new JSAct("#(#)", x, e);
        }

        public static Act Set<T>(FieldExpr<T> x, Expr<T> e)
        {
            return new JSAct("# = #", x, e);
        }

        public static Act Set<T>(FieldExpr<ObsVarRef<T>> x, Expr<T> e)
        {
            return new JSAct("#(#)", x, e);
        }

        public static Act Set<T>(ObsVarField<T> x, Expr<T> e)
        {
            // To the untrained eye, this might seem like an outrageous, shameful hack.
            return new JSAct("#" + x.Fields + "(#)", x.X, e);
        }

        /// <summary>
        /// While(cond, act) denotes this JavaScript: while (cond) { act }.
        /// </summary>
        /// <param name="cond"></param>
        /// <param name="act"></param>
        /// <returns></returns>
        public static Act While(Expr<bool> cond, Act act)
        {
            return new FmtAct(
                "While",
                new FmtIndentation(),
                new FmtStr("while "),
                new FmtParens(cond),
                new FmtStr(" "),
                Do(act)
            );
        }

        /// <summary>
        /// DoWhile(cond, act) denotes this JavaScript: do { act } while (cond);.
        /// </summary>
        /// <param name="cond"></param>
        /// <param name="act"></param>
        /// <returns></returns>
        public static Act DoWhile(Act act, Expr<bool> cond)
        {
            return new FmtAct(
                "DoWhile",
                new FmtIndentation(),
                new FmtStr("do "),
                Do(act),
                new FmtStr("while "),
                new FmtParens(cond),
                new FmtStr(";"),
                new FmtNewline()
            );
        }

        /// <summary>
        /// For(init, cond, increment, body) denotes this JavaScript:
        ///     for(init; cond; increment) { body }
        /// </summary>
        /// <param name="init"></param>
        /// <param name="cond"></param>
        /// <param name="increment"></param>
        /// <param name="body"></param>
        /// <returns></returns>
        public static Act For(Act init, Expr<bool> cond, Act increment, Act body)
        {
            return new FmtAct(
                "For",
                new FmtIndentation(),
                new FmtStr("for ("),
                init,
                new FmtStr("; "),
                cond,
                new FmtStr("; "),
                increment,
                new FmtStr(") "),
                Do(body)
            );
        }

        // XXX This is wrong!  Either need JS foreach or a for-next loop.
        //public static Act For<T>(Expr<T[]> xs, Func<Param<T>, Act> f)
        //{
        //    var x = new Param<T> { };

        //    return new FmtAct(
        //        "ForIn",
        //        new FmtIndentation(),
        //        new FmtStr("for(var "),
        //        x,
        //        new FmtStr(" in "),
        //        xs,
        //        new FmtStr(") "),
        //        Do(x)
        //    );
        //}

        /// <summary>
        /// Denotes the JavaScript continue keyword.
        /// </summary>
        /// <returns></returns>
        public static Act Continue()
        {
            return JSAct("continue");
        }

        /// <summary>
        /// Denotes the JavaScript break keyword.
        /// </summary>
        /// <returns></returns>
        public static Act Break()
        {
            return JSAct("break");
        }

        /// <summary>
        /// IfThen(cond, thenAct) denotes this JavaScript:
        ///     if (cond) { thenAct }
        /// </summary>
        /// <param name="cond"></param>
        /// <param name="thenAct"></param>
        /// <returns></returns>
        public static Act IfThen(Expr<bool> cond, Act thenAct)
        {
            return new FmtAct(
                "IfThen",
                new FmtIndentation(),
                new FmtStr("if "),
                new FmtParens(cond),
                new FmtStr(" "),
                Do(thenAct),
                new FmtNewline()
            );
        }

        /// <summary>
        /// IfThenElse(cond, thenAct, elseAct) denotes this JavaScript:
        ///     if (cond) { thenAct } else { elseAct }
        /// </summary>
        /// <param name="cond"></param>
        /// <param name="thenAct"></param>
        /// <returns></returns>
        public static Act IfThenElse(Expr<bool> cond, Act thenAct, Act elseAct)
        {
            return new FmtAct(
                "IfThenElse",
                new FmtIndentation(),
                new FmtStr("if "),
                new FmtParens(cond),
                new FmtStr(" "),
                Do(thenAct),
                new FmtIndentation(),
                new FmtStr("else "),
                Do(elseAct),
                new FmtNewline()
            );
        }

        /// <summary>
        /// A container for an IfCase WhenThen clause.
        /// </summary>
        public class WhenThen
        {
            internal Expr<bool> Cond;
            internal Act ThenAct;
        }

        /// <summary>
        /// IfCase(When(cond1, act1), When(cond2, act2), ..., When(condN, actN))
        /// denotes this JavaScript:
        ///     if (cond1) { act1 } else if (cond2) { act2 } ... else if (condN) { actN }.
        /// </summary>
        /// <param name="cases"></param>
        /// <returns></returns>
        public static Act IfCase(params WhenThen[] cases)
        {
            var terms = new List<ITerm> { };

            terms.Add(new FmtIndentation());
            for (var i = 0; i < cases.Length; i++) {
                terms.Add(new FmtStr(i == 0 ? "if (" : " else if ("));
                terms.Add(cases[i].Cond);
                terms.Add(new FmtStr(") "));
                terms.Add(Do(cases[i].ThenAct));
            }
            terms.Add(new FmtNewline());

            return new FmtAct("IfCase", terms.ToArray());
        }

        public static WhenThen When(Expr<bool> cond, Act thenAct )
        {
            return new WhenThen { Cond = cond, ThenAct = thenAct };
        }

        public static WhenThen Otherwise(Act elseAct)
        {
            return new WhenThen { Cond = true, ThenAct = elseAct };
        }

        /// <summary>
        /// Do(act1, act2, ..., actN) denotes this JavaScript: { act1; act2; ... actN; }.
        /// </summary>
        /// <param name="acts"></param>
        /// <returns></returns>
        public static Act Do(params Act[] acts)
        {
            return new DoAct { Acts = acts };
        }

        internal class DoAct : Act, ITerm
        {
            internal Act[] Acts;

            internal IEnumerable<Act> FlattenedActs()
            {
                return Acts.SelectMany(x => (x is DoAct ? ((DoAct) x).FlattenedActs() : new Act[] { x }));
            }

            //internal static int i = 1;
            void ITerm.Write(Context ctxt, JSorHtml what)
            {
                //ctxt.Write("/*{0}*/", i++);
                ctxt.WriteLine("{{");
                ctxt.Indentation++;
                foreach (var x in FlattenedActs().Cast<ITerm>()) x.Write(ctxt, what);
                ctxt.Indentation--;
                ctxt.WriteIndented("}}");
            }

            IEnumerable<IVar> ITerm.Dependencies()
            {
                return Acts.Cast<ITerm>().SelectMany(y => y.Dependencies());
            }
        }

        /// <summary>
        /// Do(enumerableOfActs) denotes this JavaScript: { act1; act2; ... actN; }
        /// where enumerableOfActs denotes the sequence act1, act2, ..., actN.
        /// </summary>
        /// <param name="acts"></param>
        /// <returns></returns>
        public static Act Do(IEnumerable<Act> acts)
        {
            return Do(acts.ToArray());
        }

        /// <summary>
        /// Local(x => body) introduces an uninitialised (i.e., with initial value undefined)
        /// local variable x in body.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Act Local<T>(Func<Param<T>, Act> f) { return Local(null, f); }

        /// <summary>
        /// Local(e, x => body) introduces a local variable x, initialised to e, in body.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="e"></param>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Act Local<T>(Expr<T> e, Func<Param<T>, Act> f)
        {
            var paramNames = funcParamNames(f);
            var x = new Param<T> { BaseName = paramNames[0] };

            return Do(JSAct("var # = #", x, e), f(x));

            //return new FmtAct(
            //    "Let",
            //    new FmtIndentation(),
            //    new FmtStr("var "),
            //    x,
            //    new FmtStr(" = "),
            //    e,
            //    new FmtStr(";"),
            //    new FmtNewline(),
            //    f(x)
            //);
        }

        /// <summary>
        /// Do(e) denotes the JavaScript: e.
        /// 
        /// This is used to evaluate an expression for its side effect; the result of
        /// the expression is ignored.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="x"></param>
        /// <returns></returns>
        public static Act Do<T>(Expr<T> x)
        {
            return JSAct("#", x);
        }

        /// <summary>
        /// Denotes the JavaScript return statement.
        /// </summary>
        /// <returns></returns>
        public static Act Return()
        {
            return JSAct("return");
        }

        /// <summary>
        /// The class of JavaScript error exceptions.
        /// </summary>
        public class JSError
        {
            public Expr<string> message;
        }

        /// <summary>
        /// Throw(excn) denotes the JavaScript: throw excn.
        /// </summary>
        /// <param name="excn"></param>
        /// <returns></returns>
        public static Act Throw(Expr<JSError> excn)
        {
            return JSAct("throw #", excn);
        }

        /// <summary>
        /// Throw(msg) denotes the JavaScript: throw { message: msg }.
        /// </summary>
        /// <param name="excn"></param>
        /// <returns></returns>
        public static Act Throw(Expr<string> msg)
        {
            return JSAct("throw { message: # }", msg);
        }

        public static Act Try<T>(Act tryAct, Func<Expr<T>, Act> catchAct = null, Act finallyAct = null)
        {
            var tryPart = new FmtAct(
                "try",
                new FmtIndentation(),
                new FmtStr("try "),
                Do(tryAct)
            );

            var x = new Param<T> { };

            var catchPart =
                ( ((object)catchAct) == null
                ? (ITerm) new FmtStr("")
                : (ITerm) new FmtAct(
                    "catch",
                    new FmtStr(" catch ("),
                    x,
                    new FmtStr(") "),
                    Do(catchAct(x))
                  )
                );

            var finallyPart =
                ( ((object)finallyAct) == null
                ? (ITerm) new FmtStr("")
                : (ITerm) new FmtAct(
                    "finally",
                    new FmtStr(" finally "),
                    Do(finallyAct)
                  )
                );

            return new FmtAct(
                "try-catch-finally",
                tryPart,
                catchPart,
                finallyPart,
                new FmtNewline()
            );
        }

        public static Act VerbatimJavaScript(string js)
        {
            return new VerbatimJavaScriptAct { JS = js };
        }

    }
}
